1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
| import com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.Claims; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.data.redis.core.ReactiveStringRedisTemplate; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.server.PathContainer; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.util.pattern.PathPattern; import org.springframework.web.util.pattern.PathPatternParser; import reactor.core.publisher.Mono; import zdemo.model.common.Result; import zdemo.model.constant.Constant; import zdemo.model.constant.ResultCode; import zdemo.utils.OwliasJwtUtil; import java.io.IOException; import java.time.Duration; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors;
@Slf4j @Component public class GatewayAuthFilter implements GlobalFilter, Ordered {
private final OwliasJwtUtil jwtUtil; private final ReactiveStringRedisTemplate reactiveRedisTemplate; private final List<PathPattern> whiteListPatterns;
private final ObjectMapper objectMapper = new ObjectMapper();
public GatewayAuthFilter(OwliasJwtUtil jwtUtil, ReactiveStringRedisTemplate reactiveRedisTemplate) { this.jwtUtil = jwtUtil; this.reactiveRedisTemplate = reactiveRedisTemplate; PathPatternParser parser = new PathPatternParser(); List<String> whiteList = Arrays.asList("/user/login", "/user/register", "/auth/refresh", "/public/**"); this.whiteListPatterns = whiteList.stream() .map(parser::parse) .collect(Collectors.toList()); }
@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); PathContainer requestPath = request.getPath().pathWithinApplication();
for (PathPattern pattern : whiteListPatterns) { if (pattern.matches(requestPath)) { return chain.filter(exchange); } }
String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION); if (authHeader == null || !authHeader.startsWith("Bearer ")) { return onError(exchange, HttpStatus.UNAUTHORIZED, ResultCode.UNAUTHORIZED, "Missing or invalid token format"); } String accessToken = authHeader.substring(7);
try { Claims claims = jwtUtil.parseExternalToken(accessToken); String tokenType = claims.get("type", String.class); if (!OwliasJwtUtil.TOKEN_TYPE_OF_ACCESS.equals(tokenType)) { return onError(exchange, HttpStatus.FORBIDDEN, ResultCode.TOKEN_INVALID, "Expected Access Token"); } String userId = claims.getSubject(); String role = claims.get("role", String.class);
return reactiveRedisTemplate.hasKey(Constant.TOKEN_BLACKLIST_USER_KEY_PREFIX + userId) .timeout(Duration.ofMillis(Constant.REDIS_FALLBACK_MILLI_SECONDS)) .onErrorResume(e -> { log.error("Redis cluster is DOWN or timout! Authentication bypassing blacklist checks, userId:{}", userId, e); return Mono.just(Boolean.FALSE); }) .flatMap(isBlack -> { if (Boolean.TRUE.equals(isBlack)) { return onError(exchange, HttpStatus.UNAUTHORIZED, ResultCode.UNAUTHORIZED, "User has been forced offline"); }
String internalToken = jwtUtil.generateInternalWatermark(userId, role); ServerHttpRequest mutatedRequest = request.mutate() .header("X-User-Id", userId) .header("X-User-Role", role) .header("X-Gateway-Token", internalToken) .headers(httpHeaders -> httpHeaders.remove(HttpHeaders.AUTHORIZATION)) .build();
return chain.filter(exchange.mutate().request(mutatedRequest).build()); }); } catch (io.jsonwebtoken.ExpiredJwtException e) { return onError(exchange, HttpStatus.UNAUTHORIZED, ResultCode.TOKEN_EXPIRED, "Access token expired, please refresh."); } catch (Exception e) { return onError(exchange, HttpStatus.UNAUTHORIZED, ResultCode.TOKEN_INVALID, "Signature verification failed"); } }
private Mono<Void> onError(ServerWebExchange exchange, HttpStatus httpStatus, ResultCode businessCode, String msg) { ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(httpStatus); response.getHeaders().setContentType(MediaType.APPLICATION_JSON); Result<Void> errorResult = Result.fail(businessCode, msg);
return response.writeWith(Mono.defer(() -> { DataBuffer buffer = response.bufferFactory().allocateBuffer(); try { objectMapper.writeValue(buffer.asOutputStream(), errorResult); return Mono.just(buffer); } catch (IOException e) { log.error("Gateway zero-copy serialize failed", e); DataBufferUtils.release(buffer); return Mono.empty(); } })); }
@Override public int getOrder() { return -1; } }
|